package com.wdl.webserver.api;

import static java.util.Arrays.asList;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.RoleResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.admin.client.resource.UsersResource;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.wdl.webserver.Utils;
import com.wdl.webserver.ZuulFilters;
import com.wdl.webserver.data.GenResponse;
import com.wdl.webserver.data.UserBase;
import com.wdl.webserver.data.UserCreation;
import com.wdl.webserver.data.UserInfo;
import com.wdl.webserver.data.UserPassword;
import com.wdl.webserver.data.UserRegister;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import springfox.documentation.annotations.ApiIgnore;

@RestController
@RequestMapping("/api/keycloak")
@Api(value="keycloak", description="User Administration")
public class KeycloakAdminController {
    private static final Logger LOG = LoggerFactory.getLogger(KeycloakAdminController.class);
    public static final String INSTITUTION_PREFIX = "###INST###";
    public static final String ROOT_INSTITUTION = "护适通";
    private static final int MAX_INSTITUTION_USERS = 10000;

    @Value("${keycloak.realm:hushitong}")
    String realm;

    @Value("${kc-api.user:admin}")
    String user;

    @Value("${kc-api.password:admin}")
    String password;

    @Value("${kc-api.url:http://localhost:8080/auth}")
    String url;

    @Value("${webserver.roles}")
    String[] ROLES_ARRAY;

    @Autowired
    CacheService cacheService;

    List<String> ROLES = new ArrayList<String>();

    private Keycloak keycloak;

    @ApiIgnore // HST creates the admin user for institution to use. Only reset-password is granted for the open APIs!!!!
    @GetMapping(value = "/users", produces = MediaType.APPLICATION_JSON)
    @ApiOperation(value = "Get institution users with pages.")
//  @PreAuthorize("hasAnyAuthority('admin', 'director', 'header', 'doctor', 'nurse')")
    public Map<String, Object> getUsers(
            @ApiParam("Institution Name, return all users if not specified.") @RequestParam("institution") Optional<String> institution,
            @ApiParam("Starts from, default to 0 if not specified.") @RequestParam("first") Optional<Integer> first,
            @ApiParam("Maximum results size (defaults to 100 if not specified)") @RequestParam("max") Optional<Integer> max,
            @ApiParam("A String contained in username, first or last name, or email") @RequestParam("search") Optional<String> search,
            HttpServletRequest request) {
        LOG.info(Utils.getLogPrefix(request) + "started");

        Map<String, Object> response = new HashMap<String, Object>();
        Map<String, Object> page = new HashMap<String, Object>();
        List<UserInfo> locals = new ArrayList<UserInfo>();

        RealmResource realmRes = this.keycloak.realm(realm);
        UsersResource usersRes = realmRes.users();

        int start = 0;
        int size = 100;
        int total = 0;

        if (first.isPresent() && first.get() >= 0) {
            start = first.get();
        }

        page.put("start", start);

        if (max.isPresent() && max.get() >= 0) {
            size = max.get();
        }

        page.put("size", size);

        List<UserRepresentation> users = usersRes.search(INSTITUTION_PREFIX, 0, MAX_INSTITUTION_USERS);
        if (users == null || users.isEmpty()) {
            page.put("total", total);

            response.put("users", locals);
            response.put("page", page);

            LOG.info(Utils.getLogPrefix(request) + "ended, no institution users found!");
            return response;
        }

        for (UserRepresentation user : users) {
            String firstName = user.getFirstName();
            if (!firstName.startsWith(INSTITUTION_PREFIX)) {
                continue;
            }

            firstName = firstName.substring(INSTITUTION_PREFIX.length());

            // check if it's for my institution
            String institutionName = institution.isPresent() ? institution.get() : "";
            if (!StringUtils.isEmpty(institutionName) && !firstName.equals(institutionName)) {
                continue;
            }

            String searchText = search.isPresent() ? search.get() : "";
            if (StringUtils.isEmpty(searchText)) {
                locals.add(buildUserInfo(usersRes, user, firstName));
                continue;
            }

            searchText = searchText.toLowerCase();
            if (this.isUserQulified(user, searchText, firstName)) {
                locals.add(buildUserInfo(usersRes, user, firstName));
            }
        }

        total = locals.size();
        page.put("total", total);
        response.put("page", page);

        List<UserInfo> userList = new ArrayList<UserInfo>();
        if (start >= total || total == 0) {
            // we're done, no new items can be returned.
            response.put("users", userList);
            LOG.info(Utils.getLogPrefix(request) + "ended, " + (total == 0 ? "no users found!" : "reached record end!"));
            return response;
        }

        for (int i = start; i < total && i < start + size; i++) {
            userList.add(locals.get(i));
        }

        response.put("users", userList);

        LOG.info(Utils.getLogPrefix(request) + "ended, total " + locals.size() + " users, returned " + userList.size() + " users.");
        return response;
    }

    private boolean isUserQulified(UserRepresentation user, String search, String firstName) {
        if (firstName.toLowerCase().contains(search)) {
            return true;
        }

        String username = user.getUsername();
        if (!StringUtils.isEmpty(username) && username.toLowerCase().contains(search)) {
            return true;
        }

        String lastName = user.getLastName();
        if (!StringUtils.isEmpty(lastName) && lastName.toLowerCase().contains(search)) {
            return true;
        }

        String email = user.getEmail();
        if (!StringUtils.isEmpty(email) && email.toLowerCase().contains(search)) {
            return true;
        }

        return false;
    }

    private UserInfo buildUserInfo(UsersResource usersRes, UserRepresentation user, String firstName) {
        UserInfo local = new UserInfo();

        local.setCreatedTimestamp(user.getCreatedTimestamp());
        local.setEmail(user.getEmail());
        local.setEnabled(user.isEnabled());
        local.setFirstName(firstName);
        local.setLastName(user.getLastName());
        local.setUsername(user.getUsername());
        local.setId(user.getId());

        Map<String, List<String>> attributes = user.getAttributes();
        List<String> cares = attributes.get("cares");
        if (cares != null) {
            local.setCares(cares);
        }

        List<RoleRepresentation> userRealmRoles = usersRes.get(user.getId()).roles().realmLevel().listAll();
        for (RoleRepresentation userRealmRole : userRealmRoles) {
            String role = userRealmRole.getName();
            if (ROLES.contains(role)) {
                local.setRole(role);
            }
        }

        return local;
    }

    @ApiIgnore // HST creates the admin user for institution to use. Only reset-password is granted for the open APIs!!!!
    @PostMapping(value="/user", produces = MediaType.APPLICATION_JSON)
    @ApiOperation(value="Create a user for an institution")
//  @PreAuthorize("hasAnyAuthority('superAdmin', 'superOperator', 'admin', 'operator', 'viewer')")
    public GenResponse createUser(
            @ApiParam("User input parameters, username and email (if presents) needs to be unique.") @RequestBody UserCreation userInput,
            HttpServletRequest request) {
        LOG.info(Utils.getLogPrefix(request) + "user=" + userInput.getUsername() + " started.");

        GenResponse result = new GenResponse();

        if (StringUtils.isEmpty(userInput.getUsername())) {
            result.setStatusInfo(Status.BAD_REQUEST);
            result.setStatus(Status.BAD_REQUEST.getStatusCode());
            LOG.info(Utils.getLogPrefix(request) + "user=" + userInput.getUsername() + " failed: username is empty!");
            return result;
        }

        // firstName is institution
        if (StringUtils.isEmpty(userInput.getFirstName())) {
            result.setStatusInfo(Status.BAD_REQUEST);
            result.setStatus(Status.BAD_REQUEST.getStatusCode());
            LOG.info(Utils.getLogPrefix(request) + "user=" + userInput.getUsername() + " failed: institution is empty!");
            return result;
        }

        RealmResource realmRes = this.keycloak.realm(realm);
        UsersResource usersRes = realmRes.users();

        UserRepresentation user = new UserRepresentation();

        if (!StringUtils.isEmpty(userInput.getPassword())) {
            CredentialRepresentation credential = new CredentialRepresentation();
            credential.setType(CredentialRepresentation.PASSWORD);
            credential.setValue(userInput.getPassword());
            credential.setTemporary(false);
            user.setCredentials(asList(credential));
        }

        user.setUsername(userInput.getUsername());
        user.setFirstName(INSTITUTION_PREFIX + userInput.getFirstName());
        user.setLastName(userInput.getLastName() == null ? "" : userInput.getLastName());
        user.setEmail(userInput.getEmail() == null ? "" : userInput.getEmail());
        user.setEnabled(true);

        Map<String, List<String>> attributes = new HashMap<String, List<String>>();
        attributes.put("instName", Arrays.asList(userInput.getFirstName()));
        if (userInput.hasCares()) {
            attributes.put("cares", userInput.getCares());
        }
        user.setAttributes(attributes);

        Response response = usersRes.create(user);
        result.setStatus(response.getStatus());
        result.setStatusInfo(response.getStatusInfo());

        if (response.getStatus() != 201) {
            LOG.error("Couldn't create user: " + response.getStatusInfo().toString());
            return result;
        } else {
            String userId = response.getLocation().getPath().replaceAll(".*/([^/]+)$", "$1");

            LOG.info("User created: " + user.getUsername() + ", userId=" + userId);

            // clean up default roles
            UserResource userRes = usersRes.get(userId);
            List<RoleRepresentation> userRealmRoles = userRes.roles().realmLevel().listAll();
            userRes.roles().realmLevel().remove(userRealmRoles);

            if (!StringUtils.isEmpty(userInput.getRole())) {
                RoleResource roleRes = realmRes.roles().get(userInput.getRole());
                try {
                    RoleRepresentation adminRep = roleRes.toRepresentation();
                    userRes.roles().realmLevel().add(asList(adminRep));
                    result.setInfo("USERID=" + userId);
                } catch (Exception e) {
                    String errorText = "Failed to assign realm role " + userInput.getRole() + " to user " + userInput.getUsername() + ". USERID=" + userId;
                    LOG.error(errorText);
                    result.setInfo(errorText);
                }
            }
        }

        LOG.info(Utils.getLogPrefix(request) + "user=" + userInput.getUsername() + " ended: status=" + result.getStatus());

        return result;
    }

    @ApiIgnore
    @PostMapping(value="/register", produces = MediaType.APPLICATION_JSON)
    @ApiOperation(value="Register a normal user")
//  @PreAuthorize("hasAnyAuthority('superAdmin', 'superOperator', 'admin', 'operator', 'viewer')")
    public GenResponse registerUser(
            @ApiParam("User input parameters") @RequestBody UserRegister userInput,
            HttpServletRequest request) {
        LOG.info(Utils.getLogPrefix(request) + "user=" + userInput.getPhoneNumber() + " started.");

        GenResponse result = new GenResponse();

        String phoneNumber = userInput.getPhoneNumber().trim();
        if (StringUtils.isEmpty(phoneNumber)) {
            result.setStatusInfo(Status.BAD_REQUEST);
            result.setStatus(Status.BAD_REQUEST.getStatusCode());
            LOG.debug(Utils.getLogPrefix(request) + "phoneNumber=" + phoneNumber + " failed: phoneNumber is empty!");
            return result;
        }

        String password = userInput.getPassword().trim();
        if (StringUtils.isEmpty(password)) {
            result.setStatusInfo(Status.BAD_REQUEST);
            result.setStatus(Status.BAD_REQUEST.getStatusCode());
            LOG.debug(Utils.getLogPrefix(request) + "phoneNumber=" + phoneNumber + " failed: password is empty!");
            return result;
        }

        String code = userInput.getCode().trim();
        String savedCode = this.cacheService.getCode(phoneNumber);
        if (StringUtils.isEmpty(code) || StringUtils.isEmpty(savedCode) || !code.equals(savedCode)) {
            result.setStatusInfo(Status.NOT_ACCEPTABLE);
            result.setStatus(Status.NOT_ACCEPTABLE.getStatusCode());
            LOG.debug(Utils.getLogPrefix(request) + "phoneNumber=" + phoneNumber + " failed: code mismatch, expected: " + savedCode + ", got " + code);
            return result;
        }

        RealmResource realmRes = this.keycloak.realm(realm);
        UsersResource usersRes = realmRes.users();

        UserRepresentation user = new UserRepresentation();

        CredentialRepresentation credential = new CredentialRepresentation();
        credential.setType(CredentialRepresentation.PASSWORD);
        credential.setValue(userInput.getPassword());
        credential.setTemporary(false);
        user.setCredentials(asList(credential));

        user.setUsername(phoneNumber);
        user.setEnabled(true);

        Response response = usersRes.create(user);
        result.setStatus(response.getStatus());
        result.setStatusInfo(response.getStatusInfo());

        if (response.getStatus() != 201) {
            LOG.error("Couldn't create user: " + response.getStatusInfo().toString());
            return result;
        } else {
            String userId = response.getLocation().getPath().replaceAll(".*/([^/]+)$", "$1");
            LOG.debug("User created: " + user.getUsername() + ", userId=" + userId);
        }

        LOG.info(Utils.getLogPrefix(request) + "phoneNumber=" + phoneNumber + " ended: status=" + result.getStatus());

        return result;
    }

    @ApiIgnore
    @PutMapping(value="/institution", produces = MediaType.APPLICATION_JSON)
    @ApiOperation(value="Update the institution name for all users")
    public GenResponse updateInstitution(
            @ApiParam("User input parameters, include oldInst and newInst names.") @RequestBody Map<String, String> input,
            HttpServletRequest request) {
        String oldInst = input.get("oldInst");
        String newInst = input.get("newInst");
        LOG.info(Utils.getLogPrefix(request) + "oldInst=" + oldInst + ", newInst=" + newInst + " started.");

        GenResponse result = new GenResponse();
        result.setStatusInfo(Status.OK);
        result.setStatus(Status.OK.getStatusCode());

        if (newInst == null || oldInst == null || newInst.isEmpty() || oldInst.isEmpty()) {
            result.setStatusInfo(Status.BAD_REQUEST);
            result.setStatus(Status.BAD_REQUEST.getStatusCode());
            result.setInfo("Invalid inputs, oldInst=" + oldInst + ", newInst=" + newInst);
            LOG.info(Utils.getLogPrefix(request) + "Invalid inputs, oldInst=" + oldInst + ", newInst=" + newInst);
            return result;
        }

        if (newInst.equals(oldInst)) {
            return result;
        }

        RealmResource realmRes = this.keycloak.realm(realm);
        UsersResource usersRes = realmRes.users();

        List<UserRepresentation> users = usersRes.search(INSTITUTION_PREFIX, 0, MAX_INSTITUTION_USERS);
        if (users == null || users.isEmpty()) {
            LOG.info(Utils.getLogPrefix(request) + "ended, no institution users found!");
            result.setInfo("No users found for institution " + oldInst);
            return result;
        }

        short total = 0;
        for (UserRepresentation user : users) {
            String firstName = user.getFirstName();
            if (!firstName.startsWith(INSTITUTION_PREFIX)) {
                continue;
            }

            firstName = firstName.substring(INSTITUTION_PREFIX.length());

            // check if it's for my institution
            if (!oldInst.equals(firstName)) {
                continue;
            }

            UserResource userRes = usersRes.get(user.getId());
            user.setFirstName(INSTITUTION_PREFIX + newInst);
            user.setAttributes(Collections.singletonMap("instName", Arrays.asList(newInst)));
            userRes.update(user);
            total++;
        }

        LOG.info(Utils.getLogPrefix(request) + "ended, total " + total + " users were modified!");
        return result;
    }

    @ApiIgnore
    @PutMapping(value="/user/{id:.+}", produces = MediaType.APPLICATION_JSON)
    @ApiOperation(value="Update a user")
    public GenResponse updateUser(
            @ApiParam("user id") @PathVariable String id,
            @ApiParam("User input parameters, username will be skipped if it presents.") @RequestBody UserBase userInput,
            HttpServletRequest request) {
        LOG.info(Utils.getLogPrefix(request) + "user=" + userInput.getUsername() + " started.");

        GenResponse result = new GenResponse();

        UserResource userRes = this.keycloak.realm(realm).users().get(id);
        if (userRes == null) {
            result.setStatusInfo(Status.NOT_FOUND);
            result.setStatus(Status.NOT_FOUND.getStatusCode());
            result.setInfo("User id " + id + "does not exist!");

            LOG.info(Utils.getLogPrefix(request) + "user id " + id + " ended: status=" + result.getStatus());
            return result;
        }

        UserRepresentation user = userRes.toRepresentation();

        String oldFirstName = user.getFirstName();
        String firstName = userInput.getFirstName();
        if (!StringUtils.isEmpty(oldFirstName) && oldFirstName.startsWith(INSTITUTION_PREFIX)) {
            // For institution users
            if (!StringUtils.isEmpty(firstName)) {
                user.setFirstName(INSTITUTION_PREFIX + firstName);

                Map<String, List<String>> attributes = new HashMap<String, List<String>>();
                attributes.put("instName", Arrays.asList(firstName));
                if (userInput.hasCares()) {
                    attributes.put("cares", userInput.getCares());
                }
                user.setAttributes(attributes);
            }
        } else {
            // For personel users
            user.setFirstName(StringUtils.isEmpty(firstName) ? "" : firstName);
        }

        user.setLastName(userInput.getLastName() == null ? "" : userInput.getLastName());
        user.setEmail(userInput.getEmail() == null ? "" : userInput.getEmail());

        userRes.update(user);

        if (!StringUtils.isEmpty(userInput.getRole())) {
            // Update role, clean up old role firstly
            List<RoleRepresentation> userRealmRoles = userRes.roles().realmLevel().listAll();
            userRes.roles().realmLevel().remove(userRealmRoles);

            RoleResource roleRes = this.keycloak.realm(realm).roles().get(userInput.getRole());
            try {
                RoleRepresentation roleRep = roleRes.toRepresentation();
                userRes.roles().realmLevel().add(asList(roleRep));
            } catch (Exception e) {
                String errorText = "Failed to assign realm role " + userInput.getRole() + " to user " + userInput.getUsername() + ". USERID=" + id;
                LOG.error(errorText);
            }
        }

        result.setStatusInfo(Status.OK);
        result.setStatus(Status.OK.getStatusCode());

        LOG.info(Utils.getLogPrefix(request) + "user=" + userInput.getUsername() + " ended: status=" + result.getStatus());

        return result;
    }

    @ApiIgnore
    @PostMapping(value="/user/{id:.+}/logout", produces = MediaType.APPLICATION_JSON)
    @ApiOperation(value="Remove all user sessions associated with a user identified by user id")
    public GenResponse logoutUser(
            @ApiParam("user id") @PathVariable String id,
            HttpServletRequest request) {
        LOG.info(Utils.getLogPrefix(request) + " started.");
        GenResponse response = new GenResponse();

        UserResource userRes = this.keycloak.realm(realm).users().get(id);
        if (userRes != null) {
            response.setStatusInfo(Status.OK);
            response.setStatus(Status.OK.getStatusCode());
            userRes.logout();
        } else {
            response.setStatusInfo(Status.OK);
            response.setStatus(Status.OK.getStatusCode());
            response.setInfo("User id " + id + "does not exist!");
        }

        LOG.info(Utils.getLogPrefix(request) + "id=" + id + " ended: status=" + response.getStatusInfo());

        return response;
    }

    @PutMapping(value="/user/{id:.+}/reset-password", produces = MediaType.APPLICATION_JSON)
    @ApiOperation(value="Reset password for a user identified by user id")
    public GenResponse resetPasswordUser(
            @ApiParam("user id") @PathVariable String id,
            @ApiParam("user password") @RequestBody UserPassword input,
            HttpServletRequest request) {
        LOG.info(Utils.getLogPrefix(request) + " started.");
        GenResponse response = new GenResponse();

        UsersResource usersRes = this.keycloak.realm(realm).users();

        CredentialRepresentation credential = new CredentialRepresentation();
        credential.setType(CredentialRepresentation.PASSWORD);
        credential.setValue(input.getPassword());
        credential.setTemporary(false);

        UserResource userRes = usersRes.get(id);
        if (userRes != null) {
            String instNameInUser = ZuulFilters.getInstitutionNameFromAuthUser(request);
            if (StringUtils.isEmpty(instNameInUser) || ZuulFilters.INST_NAME_FOR_USER_ROLE.equals(instNameInUser)) {
                throw new AuthenticationServiceException("Authorized user is empty when reset-password for user id " + id);
            }

            UserRepresentation userRep = userRes.toRepresentation();
            String instNameInRequest = userRep.getFirstName();
            instNameInRequest = instNameInRequest.substring(INSTITUTION_PREFIX.length());
            if (!ZuulFilters.INST_NAME_FOR_HUSHITONG.equals(instNameInUser) && !instNameInUser.equals(instNameInRequest)) {
                LOG.error("User in [{}] cannot perform operation in [{}]!", instNameInUser, instNameInRequest);
                throw new AuthenticationServiceException("User in " + instNameInUser + " cannot reset password for user in " + instNameInRequest);
            }

            response.setStatusInfo(Status.OK);
            response.setStatus(Status.OK.getStatusCode());
            userRes.resetPassword(credential);
        } else {
            response.setStatusInfo(Status.NOT_FOUND);
            response.setStatus(Status.NOT_FOUND.getStatusCode());
            response.setInfo("User id " + id + "does not exist!");
        }

        LOG.info(Utils.getLogPrefix(request) + "id=" + id + " ended: status=" + response.getStatusInfo());

        return response;
    }

    @ApiIgnore
    @PostMapping(value="/forget-password", produces = MediaType.APPLICATION_JSON)
    @ApiOperation(value="Reset password for a user identified by user name")
    public GenResponse forgetPasswordUser(@ApiParam("request body") @RequestBody UserRegister userInput, HttpServletRequest request) {
        LOG.info(Utils.getLogPrefix(request) + " started.");
        GenResponse result = new GenResponse();

        String phoneNumber = userInput.getPhoneNumber().trim();
        if (StringUtils.isEmpty(phoneNumber)) {
            result.setStatusInfo(Status.BAD_REQUEST);
            result.setStatus(Status.BAD_REQUEST.getStatusCode());
            LOG.debug(Utils.getLogPrefix(request) + "phoneNumber=" + phoneNumber + " failed: phoneNumber is empty!");
            return result;
        }

        String password = userInput.getPassword().trim();
        if (StringUtils.isEmpty(password)) {
            result.setStatusInfo(Status.BAD_REQUEST);
            result.setStatus(Status.BAD_REQUEST.getStatusCode());
            LOG.debug(Utils.getLogPrefix(request) + "phoneNumber=" + phoneNumber + " failed: password is empty!");
            return result;
        }

        String code = userInput.getCode().trim();
        String savedCode = this.cacheService.getCode(phoneNumber);
        if (StringUtils.isEmpty(code) || StringUtils.isEmpty(savedCode) || !code.equals(savedCode)) {
            result.setStatusInfo(Status.NOT_ACCEPTABLE);
            result.setStatus(Status.NOT_ACCEPTABLE.getStatusCode());
            LOG.debug(Utils.getLogPrefix(request) + "phoneNumber=" + phoneNumber + " failed: code mismatch, expected: " + savedCode + ", got " + code);
            return result;
        }

        List<UserRepresentation> users = this.keycloak.realm(realm).users().search(phoneNumber);
        if (users.isEmpty()) {
            result.setStatusInfo(Status.NOT_FOUND);
            result.setStatus(Status.NOT_FOUND.getStatusCode());
            result.setInfo("User " + phoneNumber + "does not exist!");
            return result;
        }

        CredentialRepresentation credential = new CredentialRepresentation();
        credential.setType(CredentialRepresentation.PASSWORD);
        credential.setValue(password);
        credential.setTemporary(false);

        UserResource userRes = this.keycloak.realm(realm).users().get(users.get(0).getId());
        if (userRes != null) {
            result.setStatusInfo(Status.OK);
            result.setStatus(Status.OK.getStatusCode());
            userRes.resetPassword(credential);
        } else {
            result.setStatusInfo(Status.NOT_FOUND);
            result.setStatus(Status.NOT_FOUND.getStatusCode());
            result.setInfo("User " + phoneNumber + "does not exist!");
        }

        LOG.info(Utils.getLogPrefix(request) + "phoneNumber=" + phoneNumber + " ended: status=" + result.getStatusInfo());

        return result;
    }

    @ApiIgnore
    @PostMapping(value="/validateCode", produces = MediaType.APPLICATION_JSON)
    @ApiOperation(value="Process code validation when try to add ,delete, bind, unbind device, and modify alarm setting")
    public GenResponse validateCode(@ApiParam("request body") @RequestBody UserRegister userInput, HttpServletRequest request) {
        LOG.info(Utils.getLogPrefix(request) + " started.");
        GenResponse result = new GenResponse();

        String phoneNumber = userInput.getPhoneNumber().trim();
        if (StringUtils.isEmpty(phoneNumber)) {
            result.setStatusInfo(Status.BAD_REQUEST);
            result.setStatus(Status.BAD_REQUEST.getStatusCode());
            LOG.debug(Utils.getLogPrefix(request) + "phoneNumber=" + phoneNumber + " failed: phoneNumber is empty!");
            return result;
        }

        String code = userInput.getCode().trim();
        String savedCode = this.cacheService.getCode(phoneNumber);
        if (StringUtils.isEmpty(code) || StringUtils.isEmpty(savedCode) || !code.equals(savedCode)) {
            result.setStatusInfo(Status.NOT_ACCEPTABLE);
            result.setStatus(Status.NOT_ACCEPTABLE.getStatusCode());
            LOG.debug(Utils.getLogPrefix(request) + "phoneNumber=" + phoneNumber + " failed: code mismatch, expected: " + savedCode + ", got " + code);
            return result;
        }

        List<UserRepresentation> users = this.keycloak.realm(realm).users().search(phoneNumber);
        if (users.isEmpty()) {
            result.setStatusInfo(Status.NOT_FOUND);
            result.setStatus(Status.NOT_FOUND.getStatusCode());
            result.setInfo("User " + phoneNumber + "does not exist!");
            return result;
        }

        UserResource userRes = this.keycloak.realm(realm).users().get(users.get(0).getId());
        if (userRes != null) {
            result.setStatusInfo(Status.OK);
            result.setStatus(Status.OK.getStatusCode());
        } else {
            result.setStatusInfo(Status.NOT_FOUND);
            result.setStatus(Status.NOT_FOUND.getStatusCode());
            result.setInfo("User " + phoneNumber + "does not exist!");
        }

        LOG.info(Utils.getLogPrefix(request) + "phoneNumber=" + phoneNumber + " ended: status=" + result.getStatusInfo());

        return result;
    }

    @ApiIgnore
    @PostMapping(value="/institution/remove", produces = MediaType.APPLICATION_JSON)
    @ApiOperation(value="Remove all users under the institution specified.")
    public GenResponse removeInstitutions(
            @ApiParam("User input parameters, instName will be included.") @RequestBody Map<String, String> input,
            HttpServletRequest request) {
        String instName = input.get("instName");
        LOG.info(Utils.getLogPrefix(request) + "instName=" + instName + " started.");

        GenResponse result = new GenResponse();
        result.setStatusInfo(Status.OK);
        result.setStatus(Status.OK.getStatusCode());

        if (instName == null || instName.isEmpty()) {
            result.setStatusInfo(Status.BAD_REQUEST);
            result.setStatus(Status.BAD_REQUEST.getStatusCode());
            result.setInfo("Invalid inputs, instName=" + instName);
            LOG.info(Utils.getLogPrefix(request) + "Invalid inputs, instName=" + instName);
            return result;
        }

        RealmResource realmRes = this.keycloak.realm(realm);
        UsersResource usersRes = realmRes.users();

        List<UserRepresentation> users = usersRes.search(INSTITUTION_PREFIX, 0, MAX_INSTITUTION_USERS);
        if (users == null || users.isEmpty()) {
            LOG.info(Utils.getLogPrefix(request) + "ended, no institution users found!");
            result.setInfo("No users found for institution " + instName);
            return result;
        }

        short total = 0;
        for (UserRepresentation user : users) {
            String firstName = user.getFirstName();
            if (!firstName.startsWith(INSTITUTION_PREFIX)) {
                continue;
            }

            firstName = firstName.substring(INSTITUTION_PREFIX.length());

            // check if it's for my institution
            if (!StringUtils.isEmpty(firstName) && !firstName.equals(instName)) {
                continue;
            }

            usersRes.delete(user.getId());
            total++;
        }

        LOG.info(Utils.getLogPrefix(request) + "ended, total " + total + " users were removed!");
        return result;
    }

    @ApiIgnore
    @DeleteMapping(value="/user/{id:.+}", produces = MediaType.APPLICATION_JSON)
    @ApiOperation(value="Remove a user with id")
//  @PreAuthorize("hasAnyAuthority('superAdmin', 'admin')")
    public GenResponse deleteUser(
            @ApiParam("user id to be removed") @PathVariable String id,
            HttpServletRequest request) {
        LOG.info(Utils.getLogPrefix(request) + " started.");

        GenResponse response = new GenResponse();

        UserResource userRes = this.keycloak.realm(realm).users().get(id);
        if (userRes == null) {
            response.setStatusInfo(Status.OK);
            response.setStatus(Status.OK.getStatusCode());
            response.setInfo("User id " + id + "does not exist!");
        } else {
            Response result = this.keycloak.realm(realm).users().delete(userRes.toRepresentation().getId());
            response.setStatus(result.getStatus());
            response.setStatusInfo(result.getStatusInfo());
        }

        LOG.info(Utils.getLogPrefix(request) + " ended: status=" + response.getStatus());

        return response;
    }

    @ApiIgnore
    @GetMapping(value="/user/{username:.+}", produces = MediaType.APPLICATION_JSON)
    @ApiOperation(value="Get user info with username")
    public UserInfo getUser(
            @ApiParam("username to get") @PathVariable String username,
            HttpServletRequest request) {
        LOG.info(Utils.getLogPrefix(request) + " started.");

        UserInfo user = new UserInfo();

        UsersResource usersRes = this.keycloak.realm(realm).users();
        List<UserRepresentation> userRes = usersRes.search(username);
        UserRepresentation userRep = this.filterUser(userRes, username);

        if (userRep == null) {
            LOG.info(Utils.getLogPrefix(request) + " ended: user=" + username + " does not exist!");
            return user;
        } else {
            user.setCreatedTimestamp(userRep.getCreatedTimestamp());
            user.setEmail(userRep.getEmail());
            user.setEnabled(userRep.isEnabled());
            user.setFirstName(userRep.getFirstName());
            user.setId(userRep.getId());
            user.setLastName(userRep.getLastName());
            user.setUsername(userRep.getUsername());

            Map<String, List<String>> attributes = userRep.getAttributes();
            List<String> cares = attributes.get("cares");
            if (cares != null) {
                user.setCares(cares);
            }

            List<RoleRepresentation> userRealmRoles = usersRes.get(userRep.getId()).roles().realmLevel().listAll();
            for (RoleRepresentation userRealmRole : userRealmRoles) {
                String role = userRealmRole.getName();
                if (ROLES.contains(role)) {
                    user.setRole(role);
                }
            }

            LOG.info(Utils.getLogPrefix(request) + " ended: user=" + user.getUsername());
            return user;
        }
    }

    public UserInfo getUserInfoFromId(String id) {
        UserInfo user = new UserInfo();

        UserResource userRes = this.keycloak.realm(realm).users().get(id);
        UserRepresentation userRep = userRes.toRepresentation();

        if (userRep == null) {
            return user;
        } else {
            user.setCreatedTimestamp(userRep.getCreatedTimestamp());
            user.setEmail(userRep.getEmail());
            user.setEnabled(userRep.isEnabled());
            user.setFirstName(userRep.getFirstName());
            user.setId(userRep.getId());
            user.setLastName(userRep.getLastName());
            user.setUsername(userRep.getUsername());

            return user;
        }
    }

    @ApiIgnore
    @GetMapping(value="/validate/{username:.+}", produces = MediaType.APPLICATION_JSON)
    @ApiOperation(value="check if user exists with username")
    public GenResponse validateUser(
            @ApiParam("username to validate") @PathVariable String username,
            HttpServletRequest request) {
        LOG.info(Utils.getLogPrefix(request) + " started.");

        GenResponse response = new GenResponse();

        UsersResource usersRes = this.keycloak.realm(realm).users();
        List<UserRepresentation> userRes = usersRes.search(username);
        UserRepresentation userRep = this.filterUser(userRes, username);

        if (userRep == null) {
            LOG.info(Utils.getLogPrefix(request) + " ended: user=" + username + " does not exist!");
            response.setStatusInfo(Status.NOT_FOUND);
            response.setStatus(Status.NOT_FOUND.getStatusCode());
        } else {
            LOG.info(Utils.getLogPrefix(request) + " ended: user=" + username);
            response.setStatusInfo(Status.OK);
            response.setStatus(Status.OK.getStatusCode());
        }

        return response;
    }

    private UserRepresentation filterUser(List<UserRepresentation> userList, String username) {
        if (userList == null || userList.isEmpty()) {
            return null;
        }

        for (UserRepresentation userRep : userList) {
            if (username.equals(userRep.getUsername())) {
                return userRep;
            }
        }

        return null;
    }

    @PostConstruct
    private void initKeyCloak() {
        LOG.info("Init Keycloak with " + user + "/" + password + " for realm " + realm);
        keycloak = Keycloak.getInstance(
            this.url,
            "master",
            this.user,
            this.password,
            "admin-cli");

        ROLES = ROLES_ARRAY == null ? new ArrayList<String>() : asList(ROLES_ARRAY);
        LOG.info("Predefined roles: " + ROLES);
    }
}
